/*
 * _OPENSSL_AES_.h
 *
 *  Created on: Aug 30, 2018
 *      Author: gaoqin
 */

#ifndef OPENSSL_AES_H_
#define OPENSSL_AES_H_
#include <openssl/evp.h>
#include <openssl/md5.h>
#include <string.h>
#ifdef WIN32
//#pragma comment(lib, "libssl.lib")
#pragma comment(lib, "libcrypto.lib")
#pragma comment(lib, "Crypt32.lib")
#endif

class _OPENSSL_AES_
{
	using BYTE = unsigned char;
	using UINT = unsigned int;
	class __CTX_RESET
	{
	private:
		EVP_CIPHER_CTX** _M_ctx;
		__CTX_RESET() = delete;
		__CTX_RESET(const __CTX_RESET&) = delete;
		__CTX_RESET& operator= (const __CTX_RESET&) = delete;
	public:
		__CTX_RESET(EVP_CIPHER_CTX** _ctx)
		{
			_M_ctx = _ctx;
		}
		~__CTX_RESET()
		{
			EVP_CIPHER_CTX_reset(*_M_ctx);
		}
	};

public:
	explicit _OPENSSL_AES_()
	: _M_aes_buff(nullptr), _M_aes_len(0), _M_base64_buff(nullptr)
	, _M_base64_len(0)
	{
		_M_aes_ctx[0] = nullptr;
		_M_aes_ctx[1] = nullptr;
		_M_base64_ctx[0] = nullptr;
		_M_base64_ctx[1] = nullptr;
	}
	~_OPENSSL_AES_()
	{
		delete[] _M_aes_buff;
		delete[] _M_base64_buff;
		EVP_CIPHER_CTX_free(_M_aes_ctx[0]);
		EVP_CIPHER_CTX_free(_M_aes_ctx[1]);
		EVP_ENCODE_CTX_free(_M_base64_ctx[0]);
		EVP_ENCODE_CTX_free(_M_base64_ctx[1]);
	}
	void setkey(const char* key)
	{
		memcpy(_M_md5_key
				, MD5((const BYTE*)key, strlen(key), nullptr)
				, MD5_DIGEST_LENGTH);
	}
	int encrypt(const BYTE* _in, UINT _in_len, BYTE** out)
	{
		return _update_final(1, _in, _in_len, out);
	}
	int decrypt(const BYTE* _in, UINT _in_len, BYTE** out)
	{
		return _update_final(0, _in, _in_len, out);
	}
	int base64_encode(const BYTE* _in, UINT _in_len, char** out, bool url_safe = false)
	{
		if (!_M_base64_ctx[1])
		{
			_M_base64_ctx[1] = EVP_ENCODE_CTX_new();
			
		}
		EVP_EncodeInit(_M_base64_ctx[1]);
		int _l = _in_len << 1;
		_re_allocate_base64_buff(_l);
		EVP_EncodeUpdate(_M_base64_ctx[1]
									, _M_base64_buff, &_l, _in, _in_len);
		if (_l < 0)
		{
			return -1;
		}
		int _tmp;
		EVP_EncodeFinal(_M_base64_ctx[1], _M_base64_buff + _l, &_tmp);
		_M_base64_buff[_l + _tmp] = 0;
		if (url_safe)
		{
			_base64_safe_handle();
		}

		*out = (char*)_M_base64_buff;
		return _l + _tmp;
	}
	int base64_decode(const char* _in, BYTE** out)
	{
		if (!_M_base64_ctx[0])
		{
			_M_base64_ctx[0] = EVP_ENCODE_CTX_new();
		}
		EVP_DecodeInit(_M_base64_ctx[0]);
		int _in_len = strlen(_in);
		int _l = _in_len;
		_re_allocate_base64_buff(_l);

		EVP_DecodeUpdate(_M_base64_ctx[0]
						, _M_base64_buff, &_l, (const BYTE*)_in, _in_len);
		if (_l < 0)
		{
			return -1;
		}

		int _tmp;
		if (EVP_DecodeFinal(_M_base64_ctx[0], _M_base64_buff + _l, &_tmp) !=1)
		{
			return -1;
		}
		_M_base64_buff[_l + _tmp] = 0;
		*out = _M_base64_buff;
		return _l + _tmp;
	}

	std::string url_encode(char const * str)
	{
		std::string tmp = "";
		size_t length = strlen(str);
		for (size_t i = 0; i < length; i++)
		{
			if (isalnum((unsigned char)str[i]) ||
				(str[i] == '-') ||
				(str[i] == '_') ||
				(str[i] == '.') ||
				(str[i] == '~'))
			{
				tmp += str[i];
			}
			else if (str[i] == ' ')
			{
				tmp += "+";
			}
			else
			{
				tmp += '%';
				tmp += _to_HEX((unsigned char)str[i] >> 4);
				tmp += _to_HEX((unsigned char)str[i] % 16);
			}
		}
		return tmp;
	}

	static char _to_hex(unsigned char x)
	{
		return  x > 9 ? x + 87 : x + 48;
	}

	static char _to_HEX(unsigned char x)
	{
		return  x > 9 ? x + 55 : x + 48;
	}

private:
	_OPENSSL_AES_(const _OPENSSL_AES_&) = delete;
	_OPENSSL_AES_& operator= (const _OPENSSL_AES_&) const = delete;

	int _update_final(bool encrypte_type
			, const BYTE* _in, UINT _in_len, BYTE** out)
	{
		int _l = ((_in_len >> 4) + 1) << 4;
		_re_allocate_aes_buff(_l);
		EVP_CIPHER_CTX** _ctx = _instance(encrypte_type);
		__CTX_RESET _ctx_reset(_ctx);
		if (EVP_CipherUpdate(*_ctx, _M_aes_buff, &_l, (const BYTE*)_in, _in_len) != 1)
		{
			return -1;
		}
		int _tmp;
		EVP_CipherFinal(*_ctx, _M_aes_buff + _l, &_tmp);
		*out = _M_aes_buff;
		return _tmp + _l;
	}
	bool _init(bool encrypte_type, EVP_CIPHER_CTX** _ctx)
	{
		if (*_ctx == nullptr)
		{
			*_ctx = EVP_CIPHER_CTX_new();
		}
		EVP_CIPHER_CTX_set_padding(*_ctx, EVP_PADDING_PKCS7);
		if (EVP_CipherInit(*_ctx, EVP_aes_128_cbc()
				, _M_md5_key, _M_md5_key, encrypte_type) != 1)
		{
			EVP_CIPHER_CTX_free(*_ctx);
			*_ctx = nullptr;
			return false;
		}
		return true;
	}
	EVP_CIPHER_CTX** _instance(bool encrypte_type)
	{
		_init(encrypte_type, &_M_aes_ctx[encrypte_type]);
		return &_M_aes_ctx[encrypte_type];
	}
	inline bool _re_allocate(BYTE** p, UINT& p_len, UINT size)
	{
		if (size > p_len)
		{
			BYTE* tmp = new BYTE[size];
			if (!tmp)
			{
				return false;
			}
			delete[] *p;
			*p = tmp;
			p_len = size;
		}
		return true;
	}
	bool _re_allocate_aes_buff(UINT size)
	{
		return _re_allocate(&_M_aes_buff, _M_aes_len, size);
	}
	bool _re_allocate_base64_buff(UINT size)
	{
		return _re_allocate(&_M_base64_buff, _M_base64_len, size);
	}

	void _base64_safe_handle()
	{
		UINT64 len = strlen((char*)_M_base64_buff);
		for (UINT64 i = 0; i < len; ++i)
		{
			switch (_M_base64_buff[i])
			{
			case '+':
				_M_base64_buff[i] = '-';
				break;
			case '/':
				_M_base64_buff[i] = '_';
				break;
			case '=':
				_M_base64_buff[i] = 0;
				return;
			}
		}
	}

	BYTE			_M_md5_key[MD5_DIGEST_LENGTH];
	BYTE*			_M_aes_buff;
	UINT			_M_aes_len;
	BYTE*			_M_base64_buff;
	UINT			_M_base64_len;

	EVP_CIPHER_CTX*	_M_aes_ctx[2];
	EVP_ENCODE_CTX*	_M_base64_ctx[2];
};

#endif /* OPENSSL_AES_H_ */
